<?php

    #-------------------------------------------------------------#
	# This file is part of the Tekuna MVC Microframework for PHP. #
	# (c) 2009 Niels Jäckel <niels.jaeckel@tekuna.org>            #
	# Please visit http://tekuna.org/ for more information.       #
	#                                                             #
	# This software is licensed under the MIT License. Please     #
	# view the LICENSE file for the full text of this license.    #
    #-------------------------------------------------------------#


	Tekuna :: load('org.tekuna.core.mvc.view.AbstractView');
	Tekuna :: load('org.tekuna.core.mvc.view.XSLTViewException');


	/**
	 * This TekunaView implementation uses XSLT templates to generate an output
	 * with given parameters. The stored key/value pairs that hold the template's
	 * contents are transformed to an intermediate XML data document.
	 *
	 * By doing this, all assigned values, arrays or objects are available with
	 * XPath within the XSLT template file. Examples:
	 *
	 * $myVar                   -> /data/myVar
	 * $myArray['myKey']        -> /data/myArray/myKey
	 * $myArray[3]              -> /data/myArray[3]
	 * $myObject -> attribute   -> /data/myObject/attribute
	 */

	class XSLTView extends AbstractView {


		/**
		 * Build a new instance of XSLTVeiw. Base directory and template
		 * file are given separately to be altered independently.
		 *
		 * This overwritten constructor checks the existence of the DOM
		 * and XSLT extensions of PHP. If one of these is not available,
		 * a TekunaApplicationException is thrown.
		 *
		 * @param string $sBaseDir the base directory of the template
		 * @param string $sTemplate the template file
		 *        (file name and format differ from TekunaView implementation to another)
		 */

		public function __construct($sBaseDir = '', $sTemplate = '') {

	    	$this -> setTemplateBaseDir($sBaseDir);
	    	$this -> setTemplate($sTemplate);

			// check XSL extension existence
			if (! extension_loaded('xsl')) {

				throw new TekunaApplicationException('XSL extension is not available.');
			}

			// check DOM extension existence
			if (! extension_loaded('dom')) {

				throw new TekunaApplicationException('DOM extension is not available.');
			}
		}


		protected function buildDataXML($mData, $iLevel = 0) {

			$sOutput = '';
			$sLevelPrefix = str_repeat("\t", $iLevel+1);

			// handle only non-scalar values ... arrays or objects
			if (is_array($mData) || is_object($mData)) {

				// iterate over array or public attributes of object
				foreach ($mData as $sKey => $mValue) {

					if (is_array($mValue) && array_is_numeric($mValue)) {

						// numeric array
						foreach ($mValue as $iInnerKey => $mInnerValue) {

							// special case: numeric array inside another numeric array
							$bNumericArrayInside = (is_array($mInnerValue) && array_is_numeric($mInnerValue));

							if ($bNumericArrayInside) {

								$sOutput .= "$sLevelPrefix<$sKey>\n";
							}

							// take same key for inner key if current key is numeric
							$sInnerKey = $sKey;
							if ($bNumericArrayInside && ! is_numeric($iInnerKey)) {

								$sInnerKey = $iInnerKey;
							}

							$sOutput .= $this -> buildDataXML(array($sInnerKey => $mInnerValue), $iLevel + (int) $bNumericArrayInside);

							if ($bNumericArrayInside) {

								$sOutput .= "$sLevelPrefix</$sKey>\n";
							}
						}
					}
					else if (is_array($mValue) || is_object($mValue)) {

						// associative array or object
						$sOutput .= "$sLevelPrefix<$sKey>\n";
						$sOutput .= $this -> buildDataXML($mValue, $iLevel+1);
						$sOutput .= "$sLevelPrefix</$sKey>\n";
					}
					else {

						// scalar value
						if ($mValue === NULL) {

							// no value -> empty tag
							$sOutput .= "$sLevelPrefix<$sKey />\n";
						}
						else {

							// handle bools with special bool strings
							if (is_bool($mValue)) {

								$mValue = ($mValue) ? 'true' : 'false';
							}

							// encode special characters
							$mValue = str_replace('&', '&amp;', $mValue);
							$mValue = str_replace('<', '&lt;', $mValue);
							$mValue = str_replace('>', '&gt;', $mValue);

							$sOutput .= "$sLevelPrefix<$sKey>$mValue</$sKey>\n";
						}
					}
				}
			}

			return $sOutput;
		}


		/**
		 * Processes the XSLTView template engine. If the template file does
		 * not exist or is not readable, a TemplateNotFoundException is thrown.
		 *
		 * @return string the output
		 */

		public function process() {

			// compose template file path
			$sTemplateFile = $this -> getTemplateBaseDir() . $this -> getTemplate();

	    	// check if template existent and readable
	    	if (! file_exists($sTemplateFile) || ! is_readable($sTemplateFile)) {

	    		throw new TemplateNotFoundException("The template file '$sTemplateFile' does not exist or is not readable.");
	    	}

			try {

				// load the XSLT stylesheet
				$objXSLT = new DOMDocument();
				$objXSLT -> load($sTemplateFile);

				// build data XML
				$sDataXML = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n";
				$sDataXML .= "<data>\n";
				$sDataXML .= $this -> buildDataXML($this -> getAllValues());
				$sDataXML .= "</data>\n";

				// load data XML
				$objXML = new DOMDocument();
				$objXML -> loadXML($sDataXML);

				// load the XSLT processor
			    $objXSLTProcessor = new XSLTProcessor();

			    // load and transform the data
			    $objXSLTProcessor -> importStyleSheet($objXSLT);
			    $sResultXML = $objXSLTProcessor -> transformToXML($objXML);
			}
			catch (Exception $e) {

				throw new XSLTViewException("Error while processing the XSL template '{$this->getTemplate()}'.", $e);
			}

			// decode special characters
			$sResultXML = str_replace('&amp;', '&', $sResultXML);
			$sResultXML = str_replace('&lt;', '<', $sResultXML);
			$sResultXML = str_replace('&gt;', '>', $sResultXML);

			return $sResultXML;
		}
	}

